package com.spzx.common.rabbitmq.config;

import com.alibaba.fastjson.JSON;
import com.spzx.common.rabbitmq.domain.SpzxMqCorrelationData;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

@Component
@Slf4j
public class RabbitmqConfig {

    @Resource
    RabbitTemplate rabbitTemplate;
    @Resource
    RedisTemplate redisTemplate;
    //我们希望知道发送的消息有没有成功发到rabbitmq服务端，避免消息发送失败丢失了
    @PostConstruct
    public void init(){
        //消息是否到达交换机的回调: 被回调时 无法直接获取发送失败的消息
        rabbitTemplate.setConfirmCallback((correlationData,ack, cause)->{
            if(ack){
                log.info("消息已到达交换机");//创建交换机
            }else{
//                log.info("消息未到达交换机: {} , {}" , cause ,correlationData );
                //获取重试对象：重试发送消息、还可以通过重试对象控制重试发送消息的次数
                rerySendMsg(correlationData.getId());
            }
        });
        //消息未达到队列的回调： 可以根据returned获取到发送失败的消息内容、交换机、路由key 使用它就可以重新发送了
        rabbitTemplate.setReturnsCallback((returned)->{
            //需要根据returned的重试对象的id来获取重试对象：重试发送消息，控制重试次数
            //returned.message.messageProperties.headers.spring_returned_message_correlation -> 4ad5e194-2753-4426-8c33-6f30875154ef
            //调用重试发送消息的方法
            rerySendMsg(returned.getMessage().getMessageProperties()
                    .getHeader("spring_returned_message_correlation"));
        });
        //如果消息发送失败，为了避免消息丢失，可以重试发送消息 提高消息发送的成功率，多次失败可以记录异常日志 以后人工介入.
    }
    //重试发送消息
    private void rerySendMsg(String correlationId) {
        //1、获取redis中的重试对象
        String key = "spzx:mq:retry:dataId:" + correlationId;
        Object o = redisTemplate.opsForValue().get(key);
        if(o==null){
            return;//结束当前方法
        }
        SpzxMqCorrelationData data = (SpzxMqCorrelationData) o;
        //2、如果不需要重试：直接结束
        if(!data.isRetry()){
            // 1、删除redis中的重试对象缓存
            redisTemplate.delete(key);
            // 2、记录消息发送失败的日志
            log.error("消息发送失败：{}", JSON.toJSONString(data));
            return; //不需要重试
        }
        //3、需要重试 但是重试次数超过允许范围 直接结束
        if(data.getRetryCount()>= data.getRetryLimit()){
            //表示消息需要重试，但是次数超过允许范围，结束：
            // 1、删除redis中的重试对象缓存
            redisTemplate.delete(key);
            // 2、记录消息发送失败的日志
            log.error("消息重试发送失败：{}", JSON.toJSONString(data));
            return;//超过允许的重试次数
        }

        //4、重试发送消息
        //4.1 更新重试对象重试的次数
        data.setRetryCount(data.getRetryCount()+1);
        redisTemplate.opsForValue().set(key , data);
        //4.2 重试发送消息
        rabbitTemplate.convertAndSend(
                data.getExchange(),
                data.getRoutingKey(),
                data.getMessage(),
                data
        );
    }


    //创建测试的交换机
    @Bean
    public Exchange spzxOrderTestExchange(){
        return ExchangeBuilder
                .topicExchange("spzx.order.test.exchange")
                .durable(true)//持久化
                .build();//创建
    }

    //订单的交换机
    @Bean
    public Exchange spzxOrderExchange(){
        return ExchangeBuilder
                .topicExchange("spzx.order.exchange")
                .durable(true)//持久化
                .build();//创建
    }
    //订单提交成功 发送消息路由key可以设置为 spzx.order.submit.success

    /*
        1、提交订单成功发送消息到spzxOrderExchange
            消息路由key:"spzx.order.submit.success"
        2、购物车  和  订单 服务中的两个队列 绑定spzxOrderExchange时 都使用了spzx.order.submit.success作为路由key
            1的消息就可以发送到他俩的队列中
        3、订单服务spzxOrderCloseQueue(不编写消费者) 接收到提交订单的消息
            队列中消息设置了过期时间： 30分钟
        4、订单服务spzxOrderCloseQueue队列中的消息30分钟后会过期
            过期的死信消息，因为spzxOrderCloseQueue绑定了死信队列
            所以消息会交给spzx.order.delay.exchange死信交换机 使用spzx.order.delay.msg 路由key发送
        5、spzxOrderDelayQueue队列使用了spzx.order.delay.msg绑定了spzx.order.delay.exchange死信交换机
                spzxOrderDelayQueue在30分钟后会接收到死信消息
        6、可以通过消费者消费spzxOrderDelayQueue队列中的消息
            相当于30分钟后执行业务：
                可以关单：
                        1、修改订单的状态为关闭状态
                        2、释放订单锁定的库存
                        .... 如果使用了优惠券 释放优惠券
                                如果有积分 扣除订单提交时奖励给用户的积分....


     */

    //业务队列 接收提交订单的消息 并设置过期时间
    @Bean
    public Queue spzxOrderCloseQueue(){
        return QueueBuilder
                .durable("spzx.order.close.queue")
                .ttl(30*60*1000)//30*60*1000) //30分钟过期时间
                //消息过期默认会丢弃，如果需要使用 可以配置死信队列
                .deadLetterExchange("spzx.order.delay.exchange") //死信交换机
                .deadLetterRoutingKey("spzx.order.delay.msg") //死信路由key
                .build();
    }
    //业务队列绑定 订单交换机
    @Bean
    public Binding spzxOrderCloseBinding(Exchange spzxOrderExchange ,
                                         Queue spzxOrderCloseQueue){
        return BindingBuilder.bind(spzxOrderCloseQueue)
                .to(spzxOrderExchange)
                .with("spzx.order.submit.success")
                .noargs();

    }


    //延迟队列  接收延迟队列过期的消息
    //1、创建延迟交换机: 用来接收一个有过期时间的队列的死掉的消息
    @Bean
    public Exchange spzxOrderDelayExchange(){
        return ExchangeBuilder
                .topicExchange("spzx.order.delay.exchange")
                .durable(true)//持久化
                .build();//创建
    }
    //2、创建延迟队列
    @Bean
    public Queue spzxOrderDelayQueue(){
        return QueueBuilder
                .durable("spzx.order.delay.queue")
                .build();
    }
    //3、延迟队列绑定延迟交换机
    @Bean
    public Binding spzxOrderDelayBinding(Exchange spzxOrderDelayExchange ,
                                        Queue spzxOrderDelayQueue){
        return BindingBuilder.bind(spzxOrderDelayQueue)
                .to(spzxOrderDelayExchange)
                .with("spzx.order.delay.msg")
                .noargs();

    }








}
